/*******************************************************************************
 * This file is part of Crunch Network.
 *
 * Crunch Network is free software: you can redistribute it and/or modify it under the
 * terms of the GNU Lesser General Public License as published by the Free Software
 * Foundation, either version 3 of the License, or (at your option) any later version.
 *
 * Crunch Network is distributed in the hope that it will be useful, but WITHOUT ANY
 * WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A
 * PARTICULAR PURPOSE.  See the GNU Lesser General Public License for more details.
 *
 * You should have received a copy of the GNU Lesser General Public License along with
 * Crunch Network.  If not, see <http://www.gnu.org/licenses/>.
 ******************************************************************************/

package com.crunch.network;

import com.crunch.network.util.BitStreamReader;
import com.crunch.network.util.BitStreamWriter;

import java.io.IOException;
import java.nio.BufferOverflowException;
import java.nio.BufferUnderflowException;

/**
 * A special message type used for state table events. This class is package-private because it should never be used
 * directly by the user.
 */
class StateTableMessage extends Message {
	/**
	 * Constructs a {@code StateTableMessage} instance. The message ID is explicitly provided because this single
	 * {@link Message} subclass is used for all state table types.
	 *
	 * @param   id the ID of the state table type associated with this message.
	 */
	public StateTableMessage(int id) {
		super(id);
	}

	/**
	 * Initializes the message for reading with a state table and ghost ID bits.
	 *
	 * @param   stateTable the state table to include in the message.
	 * @param   ghostIdBits the number of bits in the ghost ID.
	 */
	public void init(StateTable stateTable, int ghostIdBits) {
		this.stateTable = stateTable;
		this.ghostIdBits = ghostIdBits;
	}

	/**
	 * Initializes the message for writing with a state table, ghost ID, ghost ID bits, and message type.
	 *
	 * @param   stateTable the state table to include in the message.
	 * @param   ghostId the ghost ID associated with the state table.
	 * @param   ghostIdBits the number of bits in the ghost ID.
	 * @param   messageType the type of message that will be written.
	 */
	public void init(StateTable stateTable, int ghostId, int ghostIdBits, MessageType messageType) {
		this.stateTable = stateTable;
		this.ghostId = ghostId;
		this.ghostIdBits = ghostIdBits;
		this.messageType = messageType;
	}

	/**
	 * Reads a state table message, which can be either a create message, an update message, or a destroy message. See
	 * {@link Message#read(com.crunch.network.util.BitStreamReader)} for parameter and exception details.
	 */
	public void read(BitStreamReader reader)
			throws BufferUnderflowException, IOException {
		// read the ghost ID
		ghostId = (int) reader.readUnsignedInt(ghostIdBits);

		// read the type bits
		int type = (int) reader.readUnsignedInt(2);
		switch (type) {
			case MSG_CREATE_ID:
				messageType = MessageType.CREATE;
				break;
			case MSG_UPDATE_ID:
				messageType = MessageType.UPDATE;
				break;
			case MSG_DESTROY_ID:
				messageType = MessageType.DESTROY;
				break;
			default:
				throw new IOException("Invalid state table message type");
		}

		// reset status of each element
		for (int e = 0; e < stateTable.getElementCount(); ++e) {
			StateElement element = stateTable.getStateElement(e);
			element.reset();
		}

		if (messageType == MessageType.CREATE) {
			// mark all elements as having updated values for the creation message
			for (int e = 0; e < stateTable.getElementCount(); ++e) {
				StateElement element = stateTable.getStateElement(e);
				element.setUpdated();
			}
		} else if (messageType == MessageType.UPDATE) {
			// read the bitfield
			for (int e = 0; e < stateTable.getElementCount(); ++e) {
				StateElement element = stateTable.getStateElement(e);
				if (element.getNetworkTransmissionMode() == StateElement.NetworkTransmissionMode.WHEN_DIRTY) {
					// read the bitfield entry for this element
					boolean updated = reader.readBoolean();
					if (updated) {
						element.setUpdated();
					}
				} else if (element.getNetworkTransmissionMode() == StateElement.NetworkTransmissionMode.ALWAYS) {
					// always is updated
					element.setUpdated();
				}
			}
		}

		// now read the values corresponding to the elements which set called setUpdated() on
		for (int e = 0; e < stateTable.getElementCount(); ++e) {
			StateElement element = stateTable.getStateElement(e);
			if (element.isUpdated()) {
				element.read(reader);
			}
		}
	}

	/**
	 * Writes a state table message, which can be either a create message, an update message, or a destroy message. See
	 * {@link Message#write(com.crunch.network.util.BitStreamWriter)} for parameter and exception details.
	 */
	public void write(BitStreamWriter writer)
			throws BufferOverflowException {
		// write the ghost ID
		writer.writeUnsignedInt(0x00000000ffffffffL & ghostId, ghostIdBits);

		// write the type bits
		long typeBits = -1; // typeBits gets overwritten; if not, writeUnsignedInt() would throw error
		switch (messageType) {
			case CREATE:
				typeBits = MSG_CREATE_ID;
				break;
			case UPDATE:
				typeBits = MSG_UPDATE_ID;
				break;
			case DESTROY:
				typeBits = MSG_DESTROY_ID;
				break;
		}
		writer.writeUnsignedInt(typeBits, 2);

		if (messageType == MessageType.UPDATE) {
			// write a bitfield describing which state elements are included
			// no bitfield is required for CREATE messages because all state elements are sent in that case
			// elements in always and initialization only transmission mode don't need bits
			for (int e = 0; e < stateTable.getElementCount(); ++e) {
				StateElement element = stateTable.getStateElement(e);
				if (element.getNetworkTransmissionMode() == StateElement.NetworkTransmissionMode.WHEN_DIRTY) {
					writer.writeBoolean(element.isDirty());
				}
			}
		}

		if (messageType == MessageType.CREATE || messageType == MessageType.UPDATE) {
			for (int e = 0; e < stateTable.getElementCount(); ++e) {
				StateElement element = stateTable.getStateElement(e);
				if (StateElement.shouldBeSent(messageType == MessageType.CREATE, element.getNetworkTransmissionMode(), element.isDirty())) {
					element.write(writer);
				}
			}
		}
	}

	/**
	 * Calls either {@link StateTable#onCreated(int)}, {@link StateTable#onUpdated(int)}, or {@link
	 * StateTable#onDestroyed(int)} depending on the message type, passing in the ghost ID.
	 */
	public void execute() {
		switch (messageType) {
			case CREATE:
				stateTable.onCreated(ghostId);
				break;
			case UPDATE:
				stateTable.onUpdated(ghostId);
				break;
			case DESTROY:
				stateTable.onDestroyed(ghostId);
				break;
			default:
				assert false;
		}
	}

	/**
	 * Returns the state table for this message.
	 *
	 * @return  the state table for this message.
	 */
	public StateTable getStateTable() {
		return stateTable;
	}

	/**
	 * Returns the ghost ID for this message.
	 *
	 * @return  the ghost ID for this message.
	 */
	public int getGhostId() {
		return ghostId;
	}

	/**
	 * Returns the message type.
	 *
	 * @return  the message type.
	 */
	public MessageType getMessageType() {
		return messageType;
	}

	/**
	 * The type of message this instance represents.
	 */
	public enum MessageType {
		/**
		 * Indicates that a ghost is being created. All state elements are sent.
		 */
		CREATE,

		/**
		 * Indicates that a ghost is being updated. Only state elements in {@link
		 * StateElement.NetworkTransmissionMode#WHEN_DIRTY} mode and state elements in {@link
		 * StateElement.NetworkTransmissionMode#ALWAYS} mode are sent.
		 */
		UPDATE,

		/**
		 * Indicates a ghost should be destroyed. No state elements are sent.
		 */
		DESTROY
	}

	// values to identify the type of state table message
	private static final int MSG_CREATE_ID  = 0;
	private static final int MSG_UPDATE_ID  = 1;
	private static final int MSG_DESTROY_ID = 2;

	// state table included in the message
	private StateTable stateTable;
	// ghost ID for the message
	private int ghostId;
	// ghost ID bit count
	private int ghostIdBits;
	// the type of state table message this is: create ghost, update ghost, or destroy ghost
	private MessageType messageType;
}
